/*
 * jQuery contextMenu - Plugin for simple contextMenu handling
 *
 * Version: 1.5.6
 *
 * Authors: Rodney Rehm, Addy Osmani (patches for FF)
 * Web: http://medialize.github.com/jQuery-contextMenu/
 *
 * Licensed under
 *   MIT License http://www.opensource.org/licenses/mit-license
 *   GPL v3 http://opensource.org/licenses/GPL-3.0
 *
 */

(function($, undefined){

	// TODO: -
		// ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
		// create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative

// determine html5 compatibility
$.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
$.support.htmlCommand = ('HTMLCommandElement' in window);

var // currently active contextMenu trigger
	$currentTrigger = null,
	// is contextMenu initialized with at least one menu?
	initialized = false,
	// flag stating to ignore the contextmenu event
	ignoreThisClick = false,
	// window handle
	$win = $(window),
	// number of registered menus
	counter = 0,
	// mapping selector to namespace
	namespaces = {},
	// mapping namespace to options
	menus = {},
	// custom command type handlers
	types = {},
	// default values
	defaults = {
		// selector of contextMenu trigger
		selector: null,
		// where to append the menu to
		appendTo: null,
		// method to trigger context menu ["right", "left", "hover"]
		trigger: "right",
		// hide menu when mouse leaves trigger / menu elements
		autoHide: false,
		// ignore right click triggers for left, hover or custom activation
		ignoreRightClick: false,
		// ms to wait before showing a hover-triggered context menu
		delay: 200,
		// determine position to show menu at
		determinePosition: function($menu) {
			// position to the lower middle of the trigger element
			if ($.ui && $.ui.position) {
				// .position() is provided as a jQuery UI utility
				// (...and it won't work on hidden elements)
				$menu.css('display', 'block').position({
					my: "center top",
					at: "center bottom",
					of: this,
					offset: "0 5",
					collision: "fit"
				}).css('display', 'none');
			} else {
				// determine contextMenu position
				var offset = this.offset();
				offset.top += this.outerHeight();
				offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
				$menu.css(offset);
			}
		},
		// position menu
		position: function(opt, x, y) {
			var $this = this,
				offset;
			// determine contextMenu position
			if (!x && !y) {
				opt.determinePosition.call(this, opt.$menu);
				return;
			} else if (x === "maintain" && y === "maintain") {
				// x and y must not be changed (after re-show on command click)
				offset = opt.$menu.position();
			} else {
				// x and y are given (by mouse event)
				var triggerIsFixed = opt.$trigger.parents().andSelf()
					.filter(function() {
						return $(this).css('position') == "fixed";
					}).length;

				if (triggerIsFixed) {
					y -= $win.scrollTop();
					x -= $win.scrollLeft();
				}
				offset = {top: y, left: x};
			}

			// correct offset if viewport demands it
			var bottom = $win.scrollTop() + $win.height(),
				right = $win.scrollLeft() + $win.width(),
				height = opt.$menu.height(),
				width = opt.$menu.width();

			if (offset.top + height > bottom) {
				offset.top -= height;
			}

			if (offset.left + width > right) {
				offset.left -= width;
			}

			opt.$menu.css(offset);
		},
		// position the sub-menu
		positionSubmenu: function($menu) {
			if ($.ui && $.ui.position) {
				// .position() is provided as a jQuery UI utility
				// (...and it won't work on hidden elements)
				$menu.css('display', 'block').position({
					my: "left top",
					at: "right top",
					of: this,
					collision: "fit"
				}).css('display', '');
			} else {
				// determine contextMenu position
				var offset = this.offset();
				offset.top += 0;
				offset.left += this.outerWidth();
				$menu.css(offset);
			}
		},
		// offset to add to zIndex
		zIndex: 1,
		// show hide animation settings
		animation: {
			duration: 50,
			show: 'slideDown',
			hide: 'slideUp'
		},
		// events
		events: {
			show: $.noop,
			hide: $.noop
		},
		// default callback
		callback: null,
		// list of contextMenu items
		items: {}
	},
	// mouse position for hover activation
	hoveract = {
		timer: null,
		pageX: null,
		pageY: null
	},
	// determine zIndex
	zindex = function($t) {
		var zin = 0,
			$tt = $t;

		while (true) {
			zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
			$tt = $tt.parent();
			if (!$tt || !$tt.length || $tt.prop('nodeName').toLowerCase() == 'body') {
				break;
			}
		}

		return zin;
	},
	// event handlers
	handle = {
		// abort anything
		abortevent: function(e){
			e.preventDefault();
			e.stopImmediatePropagation();
		},

		// contextmenu show dispatcher
		contextmenu: function(e) {
			var $this = $(this);
			// disable actual context-menu
			e.preventDefault();
			e.stopImmediatePropagation();

			// ignore right click trigger
			if (ignoreThisClick) {
				ignoreThisClick = false;
				return;
			}

			if (!$this.hasClass('context-menu-disabled')) {
				// theoretically need to fire a show event at <menu>
				// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
				// var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
				// e.data.$menu.trigger(evt);

				$currentTrigger = $this;
				if (e.data.build) {
					// dynamically build menu on invocation
					$.extend(true, e.data, defaults, e.data.build($currentTrigger, e) || {});
					op.create(e.data);
				}
				// show menu
				op.show.call($this, e.data, e.pageX, e.pageY);
			}
		},
		// contextMenu left-click trigger
		click: function(e) {
			e.preventDefault();
			e.stopImmediatePropagation();
			$(this).trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
		},
		// contextMenu right-click trigger
		mousedown: function(e) {
			// register mouse down
			var $this = $(this);

			// hide any previous menus
			if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
				$currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
			}

			// activate on right click
			if (e.button == 2) {
				$currentTrigger = $this.data('contextMenuActive', true);
			}
		},
		// contextMenu right-click trigger
		mouseup: function(e) {
			// show menu
			var $this = $(this);
			if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
				e.preventDefault();
				e.stopImmediatePropagation();
				$currentTrigger = $this;
				$this.trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
			}

			$this.removeData('contextMenuActive');
		},
		// contextMenu hover trigger
		mouseenter: function(e) {
			var $this = $(this),
				$related = $(e.relatedTarget),
				$document = $(document);

			// abort if we're coming from a menu
			if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
				return;
			}

			// abort if a menu is shown
			if ($currentTrigger && $currentTrigger.length) {
				return;
			}

			hoveract.pageX = e.pageX;
			hoveract.pageY = e.pageY;
			hoveract.data = e.data;
			$document.on('mousemove.contextMenuShow', handle.mousemove);
			hoveract.timer = setTimeout(function() {
				hoveract.timer = null;
				$document.off('mousemove.contextMenuShow');
				$currentTrigger = $this;
				$this.trigger(jQuery.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
			}, e.data.delay );
		},
		// contextMenu hover trigger
		mousemove: function(e) {
			hoveract.pageX = e.pageX;
			hoveract.pageY = e.pageY;
		},
		// contextMenu hover trigger
		mouseleave: function(e) {
			// abort if we're leaving for a menu
			var $related = $(e.relatedTarget);
			if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
				return;
			}

			try {
				clearTimeout(hoveract.timer);
			} catch(e) {}

			hoveract.timer = null;
		},

		// ignore right click trigger
		ignoreRightClick: function(e) {
			if (e.button == 2) {
				ignoreThisClick = true;
			}
		},

		// click on layer to hide contextMenu
		layerClick: function(e) {
			var $this = $(this),
				root = $this.data('contextMenuRoot');

			e.preventDefault();
			e.stopImmediatePropagation();
			$this.remove();
			root.$menu.trigger('contextmenu:hide');
		},
		// key handled :hover
		keyStop: function(e, opt) {
			if (!opt.isInput) {
				e.preventDefault();
			}

			e.stopPropagation();
		},
		key: function(e) {
			var opt = $currentTrigger.data('contextMenu') || {},
				$children = opt.$menu.children(),
				$round;

			switch (e.keyCode) {
				case 9:
				case 38: // up
					handle.keyStop(e, opt);
					// if keyCode is [38 (up)] or [9 (tab) with shift]
					if (opt.isInput) {
						if (e.keyCode == 9 && e.shiftKey) {
							e.preventDefault();
							opt.$selected && opt.$selected.find('input, textarea, select').blur();
							opt.$menu.trigger('prevcommand');
							return;
						} else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
							// checkboxes don't capture this key
							e.preventDefault();
							return;
						}
					} else if (e.keyCode != 9 || e.shiftKey) {
						opt.$menu.trigger('prevcommand');
						return;
					}

				case 9: // tab
				case 40: // down
					handle.keyStop(e, opt);
					if (opt.isInput) {
						if (e.keyCode == 9) {
							e.preventDefault();
							opt.$selected && opt.$selected.find('input, textarea, select').blur();
							opt.$menu.trigger('nextcommand');
							return;
						} else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
							// checkboxes don't capture this key
							e.preventDefault();
							return;
						}
					} else {
						opt.$menu.trigger('nextcommand');
						return;
					}
					break;

				case 37: // left
					handle.keyStop(e, opt);
					if (opt.isInput || !opt.$selected || !opt.$selected.length) {
						break;
					}

					if (!opt.$selected.parent().hasClass('context-menu-root')) {
						var $parent = opt.$selected.parent().parent();
						opt.$selected.trigger('contextmenu:blur');
						opt.$selected = $parent;
						return;
					}
					break;

				case 39: // right
					handle.keyStop(e, opt);
					if (opt.isInput || !opt.$selected || !opt.$selected.length) {
						break;
					}

					var itemdata = opt.$selected.data('contextMenu') || {};
					if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
						opt.$selected = null;
						itemdata.$selected = null;
						itemdata.$menu.trigger('nextcommand');
						return;
					}
					break;

				case 13: // enter
					handle.keyStop(e, opt);
					if (opt.isInput) {
						if (opt.$selected && !opt.$selected.is(':textarea, :select')) {
							e.preventDefault();
							return;
						}
						break;
					}
					opt.$selected && opt.$selected.trigger('mouseup');
					return;

				case 32: // space
					// prevent browser from scrolling down while menu is visible
					handle.keyStop(e, opt);
					return;

				case 27: // esc
					handle.keyStop(e, opt);
					opt.$menu.trigger('contextmenu:hide');
					return;

				default: // 0-9, a-z
					var k = (String.fromCharCode(e.keyCode)).toUpperCase();
					if (opt.accesskeys[k]) {
						// according to the specs accesskeys must be invoked immediately
						opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
							? 'contextmenu:focus'
							: 'mouseup'
						);
						return;
					}
					break;
			}
			// pass event to selected item,
			// stop propagation to avoid endless recursion
			e.stopPropagation();
			opt.$selected && opt.$selected.trigger(e);
		},

		// select previous possible command in menu
		prevItem: function(e) {
			e.stopPropagation();
			var opt = $(this).data('contextMenu') || {};

			// obtain currently selected menu
			if (opt.$selected) {
				var $s = opt.$selected;
				opt = opt.$selected.parent().data('contextMenu') || {};
				opt.$selected = $s;
			}

			var $children = opt.$menu.children(),
				$prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
				$round = $prev;

			// skip disabled
			while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
				if ($prev.prev().length) {
					$prev = $prev.prev();
				} else {
					$prev = $children.last();
				}
				if ($prev.is($round)) {
					// break endless loop
					return;
				}
			}

			// leave current
			if (opt.$selected) {
				handle.itemMouseleave.call(opt.$selected.get(0), e);
			}

			// activate next
			handle.itemMouseenter.call($prev.get(0), e);

			// focus input
			var $input = $prev.find('input, textarea, select');
			if ($input.length) {
				$input.focus();
			}
		},
		// select next possible command in menu
		nextItem: function(e) {
			e.stopPropagation();
			var opt = $(this).data('contextMenu') || {};

			// obtain currently selected menu
			if (opt.$selected) {
				var $s = opt.$selected;
				opt = opt.$selected.parent().data('contextMenu') || {};
				opt.$selected = $s;
			}

			var $children = opt.$menu.children(),
				$next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
				$round = $next;

			// skip disabled
			while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
				if ($next.next().length) {
					$next = $next.next();
				} else {
					$next = $children.first();
				}
				if ($next.is($round)) {
					// break endless loop
					return;
				}
			}

			// leave current
			if (opt.$selected) {
				handle.itemMouseleave.call(opt.$selected.get(0), e);
			}

			// activate next
			handle.itemMouseenter.call($next.get(0), e);

			// focus input
			var $input = $next.find('input, textarea, select');
			if ($input.length) {
				$input.focus();
			}
		},

		// flag that we're inside an input so the key handler can act accordingly
		focusInput: function(e) {
			var $this = $(this).closest('.context-menu-item'),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			root.$selected = opt.$selected = $this;
			root.isInput = opt.isInput = true;
		},
		// flag that we're inside an input so the key handler can act accordingly
		blurInput: function(e) {
			var $this = $(this).closest('.context-menu-item'),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			root.isInput = opt.isInput = false;
		},

		// :hover on menu
		menuMouseenter: function(e) {
			var root = $(this).data().contextMenuRoot;
			root.hovering = true;
		},
		// :hover on menu
		menuMouseleave: function(e) {
			var root = $(this).data().contextMenuRoot;
			if (root.$layer && root.$layer.is(e.relatedTarget)) {
				root.hovering = false;
			}
		},


		// :hover done manually so key handling is possible
		itemMouseenter: function(e) {
			var $this = $(this),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			root.hovering = true;

			// abort if we're re-entering
			if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
				e.preventDefault();
				e.stopImmediatePropagation();
			}

			// make sure only one item is selected
			(opt.$menu ? opt : root).$menu
				.children('.hover').trigger('contextmenu:blur');

			if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
				opt.$selected = null;
				return;
			}

			$this.trigger('contextmenu:focus');
		},
		// :hover done manually so key handling is possible
		itemMouseleave: function(e) {
			var $this = $(this),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
				root.$selected && root.$selected.trigger('contextmenu:blur');
				e.preventDefault();
				e.stopImmediatePropagation();
				root.$selected = opt.$selected = opt.$node;
				return;
			}

			$this.trigger('contextmenu:blur');
		},
		// contextMenu item click
		itemClick: function(e) {
			var $this = $(this),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot,
				key = data.contextMenuKey,
				callback;

			// abort if the key is unknown or disabled
			if (!opt.items[key] || $this.hasClass('disabled')) {
				return;
			}

			e.preventDefault();
			e.stopImmediatePropagation();

			if ($.isFunction(root.callbacks[key])) {
				// item-specific callback
				callback = root.callbacks[key];
			} else if ($.isFunction(root.callback)) {
				// default callback
				callback = root.callback;
			} else {
				// no callback, no action
				return;
			}

			// hide menu if callback doesn't stop that
			if (callback.call(root.$trigger, key, root) !== false) {
				root.$menu.trigger('contextmenu:hide');
			} else {
				op.update.call(root.$trigger, root);
			}
		},
		// ignore click events on input elements
		inputClick: function(e) {
			e.stopImmediatePropagation();
		},

		// hide <menu>
		hideMenu: function(e) {
			var root = $(this).data('contextMenuRoot');
			op.hide.call(root.$trigger, root);
		},
		// focus <command>
		focusItem: function(e) {
			e.stopPropagation();
			var $this = $(this),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			$this.addClass('hover')
				.siblings('.hover').trigger('contextmenu:blur');

			// remember selected
			opt.$selected = root.$selected = $this;

			// position sub-menu - do after show so dumb $.ui.position can keep up
			if (opt.$node) {
				root.positionSubmenu.call(opt.$node, opt.$menu);
			}
		},
		// blur <command>
		blurItem: function(e) {
			e.stopPropagation();
			var $this = $(this),
				data = $this.data(),
				opt = data.contextMenu,
				root = data.contextMenuRoot;

			$this.removeClass('hover');
			opt.$selected = null;
		}
	},
	// operations
	op = {
		show: function(opt, x, y) {
			var $this = $(this),
				offset,
				css = {};

			// hide any open menus
			$('#context-menu-layer').trigger('mousedown');

			// show event
			if (opt.events.show.call($this, opt) === false) {
				$currentTrigger = null;
				return;
			}

			// backreference for callbacks
			opt.$trigger = $this;

			// create or update context menu
			op.update.call($this, opt);

			// position menu
			opt.position.call($this, opt, x, y);

			// make sure we're in front
			if (opt.zIndex) {
				css.zIndex = zindex($this) + opt.zIndex;
			}

			// add layer
			op.layer.call(opt.$menu, opt, css.zIndex);

			// adjust sub-menu zIndexes
			opt.$menu.find('ul').css('zIndex', css.zIndex + 1);

			// position and show context menu
			opt.$menu.css( css )[opt.animation.show](opt.animation.duration);
			// make options available
			$this.data('contextMenu', opt);
			// register key handler
			$(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
			// register autoHide handler
			if (opt.autoHide) {
				// trigger element coordinates
				var pos = $this.position();
				pos.right = pos.left + $this.outerWidth();
				pos.bottom = pos.top + this.outerHeight();
				// mouse position handler
				$(document).on('mousemove.contextMenuAutoHide', function(e) {
					if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
						// if mouse in menu...¦
						opt.$layer.trigger('mousedown');
					}
				});
			}
		},
		hide: function(opt) {
			var $this = $(this);
			if (!opt) {
				opt = $this.data('contextMenu') || {};
			}

			// hide event
			if (opt.events && opt.events.hide.call($this, opt) === false) {
				return;
			}

			if (opt.$layer) {
				try {
					opt.$layer.remove();
					delete opt.$layer;
				} catch(e) {
					opt.$layer = null;
				}
			}

			// remove handle
			$currentTrigger = null;
			// remove selected
			opt.$menu.find('.hover').trigger('contextmenu:blur');
			opt.$selected = null;
			// unregister key and mouse handlers
			//$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
			$(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
			// hide menu
			opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration);

			// tear down dynamically built menu
			if (opt.build) {
				opt.$menu.remove();
				$.each(opt, function(key, value) {
					switch (key) {
						case 'ns':
						case 'selector':
						case 'build':
						case 'trigger':
						case 'ignoreRightClick':
							return true;

						default:
							opt[key] = undefined;
							try {
								delete opt[key];
							} catch (e) {}
							return true;
				   }
				});
			}
		},
		create: function(opt, root) {
			if (root === undefined) {
				root = opt;
			}
			// create contextMenu
			opt.$menu = $('<ul class="context-menu-list ' + (opt.className || "") + '"></ul>').data({
				'contextMenu': opt,
				'contextMenuRoot': root
			});

			$.each(['callbacks', 'commands', 'inputs'], function(i,k){
				opt[k] = {};
				if (!root[k]) {
					root[k] = {};
				}
			});

			root.accesskeys || (root.accesskeys = {});

			// create contextMenu items
			$.each(opt.items, function(key, item){
				var $t = $('<li class="context-menu-item ' + (item.className || "") +'"></li>'),
					$label = null,
					$input = null;

				item.$node = $t.data({
					'contextMenu': opt,
					'contextMenuRoot': root,
					'contextMenuKey': key
				});

				// register accesskey
				// NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
				if (item.accesskey) {
					var aks = splitAccesskey(item.accesskey);
					for (var i=0, ak; ak = aks[i]; i++) {
						if (!root.accesskeys[ak]) {
							root.accesskeys[ak] = item;
							item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
							break;
						}
					}
				}

				if (typeof item == "string") {
					$t.addClass('context-menu-separator not-selectable');
				} else if (item.type && types[item.type]) {
					// run custom type handler
					types[item.type].call($t, item, opt, root);
					// register commands
					$.each([opt, root], function(i,k){
						k.commands[key] = item;
						if ($.isFunction(item.callback)) {
							k.callbacks[key] = item.callback;
						}
					});
				} else {
					// add label for input
					if (item.type == 'html') {
						$t.addClass('context-menu-html not-selectable');
					} else if (item.type) {
						$label = $('<label></label>').appendTo($t);
						$('<span></span>').html(item._name || item.name).appendTo($label);
						$t.addClass('context-menu-input');
						opt.hasTypes = true;
						$.each([opt, root], function(i,k){
							k.commands[key] = item;
							k.inputs[key] = item;
						});
					} else if (item.items) {
						item.type = 'sub';
					}

					switch (item.type) {
						case 'text':
							$input = $('<input type="text" value="1" name="context-menu-input-'+ key +'" value="">')
								.val(item.value || "").appendTo($label);
							break;

						case 'textarea':
							$input = $('<textarea name="context-menu-input-'+ key +'"></textarea>')
								.val(item.value || "").appendTo($label);

							if (item.height) {
								$input.height(item.height);
							}
							break;

						case 'checkbox':
							$input = $('<input type="checkbox" value="1" name="context-menu-input-'+ key +'" value="">')
								.val(item.value || "").prop("checked", !!item.selected).prependTo($label);
							break;

						case 'radio':
							$input = $('<input type="radio" value="1" name="context-menu-input-'+ item.radio +'" value="">')
								.val(item.value || "").prop("checked", !!item.selected).prependTo($label);
							break;

						case 'select':
							$input = $('<select name="context-menu-input-'+ key +'">').appendTo($label);
							if (item.options) {
								$.each(item.options, function(value, text) {
									$('<option></option>').val(value).text(text).appendTo($input);
								});
								$input.val(item.selected);
							}
							break;

						case 'sub':
							$('<span></span>').html(item._name || item.name).appendTo($t);
							item.appendTo = item.$node;
							op.create(item, root);
							$t.data('contextMenu', item).addClass('context-menu-submenu');
							item.callback = null;
							break;

						case 'html':
							$(item.html).appendTo($t);
							break;

						default:
							$.each([opt, root], function(i,k){
								k.commands[key] = item;
								if ($.isFunction(item.callback)) {
									k.callbacks[key] = item.callback;
								}
							});

							$('<span></span>').html(item._name || item.name || "").appendTo($t);
							break;
					}

					// disable key listener in <input>
					if (item.type && item.type != 'sub' && item.type != 'html') {
						$input
							.on('focus', handle.focusInput)
							.on('blur', handle.blurInput);

						if (item.events) {
							$input.on(item.events);
						}
					}

					// add icons
					if (item.icon) {
						$t.addClass("icon icon-" + item.icon);
					}
				}

				// cache contained elements
				item.$input = $input;
				item.$label = $label;

				// attach item to menu
				$t.appendTo(opt.$menu);

				// Disable text selection
				if (!opt.hasTypes) {
					if($.browser.msie) {
						$t.on('selectstart.disableTextSelect', handle.abortevent);
					} else if(!$.browser.mozilla) {
						$t.on('mousedown.disableTextSelect', handle.abortevent);
					}
				}
			});
			// attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
			if (!opt.$node) {
				opt.$menu.css('display', 'none').addClass('context-menu-root');
			}
			opt.$menu.appendTo(opt.appendTo || document.body);
		},
		update: function(opt, root) {
			var $this = this;
			if (root === undefined) {
				root = opt;
				// determine widths of submenus, as CSS won't grow them automatically
				// position:absolute > position:absolute; min-width:100; max-width:200; results in width: 100;
				// kinda sucks hard...
				opt.$menu.find('ul').andSelf().css({position: 'static', display: 'block'}).each(function(){
					var $this = $(this);
					$this.width($this.css('position', 'absolute').width())
						.css('position', 'static');
				}).css({position: '', display: ''});
			}
			// re-check disabled for each item
			opt.$menu.children().each(function(){
				var $item = $(this),
					key = $item.data('contextMenuKey'),
					item = opt.items[key],
					disabled = ($.isFunction(item.disabled) && item.disabled.call($this, key, root)) || item.disabled === true;

				// dis- / enable item
				$item[disabled ? 'addClass' : 'removeClass']('disabled');

				if (item.type) {
					// dis- / enable input elements
					$item.find('input, select, textarea').prop('disabled', disabled);

					// update input states
					switch (item.type) {
						case 'text':
						case 'textarea':
							item.$input.val(item.value || "");
							break;

						case 'checkbox':
						case 'radio':
							item.$input.val(item.value || "").prop('checked', !!item.selected);
							break;

						case 'select':
							item.$input.val(item.selected || "");
							break;
					}
				}

				if (item.$menu) {
					// update sub-menu
					op.update.call($this, item, root);
				}
			});
		},
		layer: function(opt, zIndex) {
			// add transparent layer for click area
			// filter and background for Internet Explorer, Issue #23
			return opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
				.css({height: $win.height(), width: $win.width(), display: 'block'})
				.data('contextMenuRoot', opt)
				.insertBefore(this)
				.on('mousedown', handle.layerClick);
		}
	};

// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
function splitAccesskey(val) {
	var t = val.split(/\s+/),
		keys = [];

	for (var i=0, k; k = t[i]; i++) {
		k = k[0].toUpperCase(); // first character only
		// theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
		// a map to look up already used access keys would be nice
		keys.push(k);
	}

	return keys;
}

// handle contextMenu triggers
$.fn.contextMenu = function(operation) {
	if (operation === undefined) {
		this.first().trigger('contextmenu');
	} else if (operation.x && operation.y) {
		this.first().trigger(jQuery.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
	} else if (operation === "hide") {
		var $menu = this.data('contextMenu').$menu;
		$menu && $menu.trigger('contextmenu:hide');
	} else if (operation) {
		this.removeClass('context-menu-disabled');
	} else if (!operation) {
		this.addClass('context-menu-disabled');
	}

	return this;
};

// manage contextMenu instances
$.contextMenu = function(operation, options) {
	if (typeof operation != 'string') {
		options = operation;
		operation = 'create';
	}

	if (typeof options == 'string') {
		options = {selector: options};
	} else if (options === undefined) {
		options = {};
	}

	// merge with default options
	var o = $.extend(true, {}, defaults, options || {}),
		$body = $body = $(document);

	switch (operation) {
		case 'create':
			// no selector no joy
			if (!o.selector) {
				throw new Error('No selector specified');
			}
			// make sure internal classes are not bound to
			if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
				throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
			}
			if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
				throw new Error('No Items sepcified');
			}
			counter ++;
			o.ns = '.contextMenu' + counter;
			namespaces[o.selector] = o.ns;
			menus[o.ns] = o;

			if (!initialized) {
				// make sure item click is registered first
				$body
					.on({
						'contextmenu:hide.contextMenu': handle.hideMenu,
						'prevcommand.contextMenu': handle.prevItem,
						'nextcommand.contextMenu': handle.nextItem,
						'contextmenu.contextMenu': handle.abortevent,
						'mouseenter.contextMenu': handle.menuMouseenter,
						'mouseleave.contextMenu': handle.menuMouseleave
					}, '.context-menu-list')
					.on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
					.on({
						'mouseup.contextMenu': handle.itemClick,
						'contextmenu:focus.contextMenu': handle.focusItem,
						'contextmenu:blur.contextMenu': handle.blurItem,
						'contextmenu.contextMenu': handle.abortevent,
						'mouseenter.contextMenu': handle.itemMouseenter,
						'mouseleave.contextMenu': handle.itemMouseleave
					}, '.context-menu-item');

				initialized = true;
			}

			// engage native contextmenu event
			$body
				.on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);

			switch (o.trigger) {
				case 'hover':
						$body
							.on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
							.on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
					break;

				case 'left':
						$body.on('click' + o.ns, o.selector, o, handle.click);
					break;
				/*
				default:
					// http://www.quirksmode.org/dom/events/contextmenu.html
					$body
						.on('mousedown' + o.ns, o.selector, o, handle.mousedown)
						.on('mouseup' + o.ns, o.selector, o, handle.mouseup);
					break;
				*/
			}

			if (o.trigger != 'hover' && o.ignoreRightClick) {
				$body.on('mousedown' + o.ns, o.selector, handle.ignoreRightClick);
			}

			// create menu
			if (!o.build) {
				op.create(o);
			}
			break;

		case 'destroy':
			if (!o.selector) {
				$body.off('.contextMenu .contextMenuAutoHide');
				$.each(namespaces, function(key, value) {
					$body.off(value);
				});

				namespaces = {};
				menus = {};
				counter = 0;
				initialized = false;

				$('.context-menu-list').remove();
			} else if (namespaces[o.selector]) {
				try {
					if (menus[namespaces[o.selector]].$menu) {
						menus[namespaces[o.selector]].$menu.remove();
					}

					delete menus[namespaces[o.selector]];
				} catch(e) {
					menus[namespaces[o.selector]] = null;
				}

				$body.off(namespaces[o.selector]);
			}
			break;

		case 'html5':
			// if <command> or <menuitem> are not handled by the browser,
			// or options was a bool true,
			// initialize $.contextMenu for them
			if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
				$('menu[type="context"]').each(function() {
					if (this.id) {
						$.contextMenu({
							selector: '[contextmenu=' + this.id +']',
							items: $.contextMenu.fromMenu(this)
						});
					}
				}).css('display', 'none');
			}
			break;

		default:
			throw new Error('Unknown operation "' + operation + '"');
	}

	return this;
};

// import values into <input> commands
$.contextMenu.setInputValues = function(opt, data) {
	if (data === undefined) {
		data = {};
	}

	$.each(opt.inputs, function(key, item) {
		switch (item.type) {
			case 'text':
			case 'textarea':
				item.value = data[key] || "";
				break;

			case 'checkbox':
				item.selected = data[key] ? true : false;
				break;

			case 'radio':
				item.selected = (data[item.radio] || "") == item.value ? true : false;
				break;

			case 'select':
				item.selected = data[key] || "";
				break;
		}
	});
};

// export values from <input> commands
$.contextMenu.getInputValues = function(opt, data) {
	if (data === undefined) {
		data = {};
	}

	$.each(opt.inputs, function(key, item) {
		switch (item.type) {
			case 'text':
			case 'textarea':
			case 'select':
				data[key] = item.$input.val();
				break;

			case 'checkbox':
				data[key] = item.$input.prop('checked');
				break;

			case 'radio':
				if (item.$input.prop('checked')) {
					data[item.radio] = item.value;
				}
				break;
		}
	});

	return data;
};

// find <label for="xyz">
function inputLabel(node) {
	return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
}

// convert <menu> to items object
function menuChildren(items, $children, counter) {
	if (!counter) {
		counter = 0;
	}

	$children.each(function() {
		var $node = $(this),
			node = this,
			nodeName = this.nodeName.toLowerCase(),
			label,
			item;

		// extract <label><input>
		if (nodeName == 'label' && $node.find('input, textarea, select').length) {
			label = $node.text();
			$node = $node.children().first();
			node = $node.get(0);
			nodeName = node.nodeName.toLowerCase();
		}

		/*
		 * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
		 * Not being the sadistic kind, $.contextMenu only accepts:
		 * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
		 * Everything else will be imported as an html node, which is not interfaced with contextMenu.
		 */

		// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
		switch (nodeName) {
			// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
			case 'menu':
				item = {name: $node.attr('label'), items: {}};
				menuChildren(item.items, $node.children(), counter);
				break;

			// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
			case 'a':
			// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
			case 'button':
				item = {
					name: $node.text(),
					disabled: !!$node.attr('disabled'),
					callback: (function(){ return function(){ $node.click(); }; })()
				};
				break;

			// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command

			case 'menuitem':
			case 'command':
				switch ($node.attr('type')) {
					case undefined:
					case 'command':
					case 'menuitem':
						item = {
							name: $node.attr('label'),
							disabled: !!$node.attr('disabled'),
							callback: (function(){ return function(){ $node.click(); }; })()
						};
						break;

					case 'checkbox':
						item = {
							type: 'checkbox',
							disabled: !!$node.attr('disabled'),
							name: $node.attr('label'),
							selected: !!$node.attr('checked')
						};
						break;

					case 'radio':
						item = {
							type: 'radio',
							disabled: !!$node.attr('disabled'),
							name: $node.attr('label'),
							radio: $node.attr('radiogroup'),
							value: $node.attr('id'),
							selected: !!$node.attr('checked')
						};
						break;

					default:
						item = undefined;
				}
				break;

			case 'hr':
				item = '-------';
				break;

			case 'input':
				switch ($node.attr('type')) {
					case 'text':
						item = {
							type: 'text',
							name: label || inputLabel(node),
							disabled: !!$node.attr('disabled'),
							value: $node.val()
						};
						break;

					case 'checkbox':
						item = {
							type: 'checkbox',
							name: label || inputLabel(node),
							disabled: !!$node.attr('disabled'),
							selected: !!$node.attr('checked')
						};
						break;

					case 'radio':
						item = {
							type: 'radio',
							name: label || inputLabel(node),
							disabled: !!$node.attr('disabled'),
							radio: !!$node.attr('name'),
							value: $node.val(),
							selected: !!$node.attr('checked')
						};
						break;

					default:
						item = undefined;
						break;
				}
				break;

			case 'select':
				item = {
					type: 'select',
					name: label || inputLabel(node),
					disabled: !!$node.attr('disabled'),
					selected: $node.val(),
					options: {}
				};
				$node.children().each(function(){
					item.options[this.value] = $(this).text();
				});
				break;

			case 'textarea':
				item = {
					type: 'textarea',
					name: label || inputLabel(node),
					disabled: !!$node.attr('disabled'),
					value: $node.val()
				};
				break;

			case 'label':
				break;

			default:
				item = {type: 'html', html: $node.clone(true)};
				break;
		}

		if (item) {
			counter++;
			items['key' + counter] = item;
		}
	});
}

// convert html5 menu
$.contextMenu.fromMenu = function(element) {
	var $this = $(element),
		items = {};

	menuChildren(items, $this.children());

	return items;
};

// make defaults accessible
$.contextMenu.defaults = defaults;
$.contextMenu.types = types;

})(jQuery);
